package org.yagnus.langutils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.regex.Pattern;

public class Reflections {

	public static Collection<String> getClasses(String pkgnm, String regexp) {
		Set<String> allClasses = new HashSet<String>();
		allClasses.addAll(getJarClasses(regexp));
		allClasses.addAll(getResourseClasses(pkgnm));
		return allClasses;
	}

	public static <T> Collection<Class<T>> getClasses(String pkgnm,
			String regexp, Class<T> superInterface) {
		return getClasses(pkgnm, regexp, superInterface, true);
	}

	/**
	 * all super interfaces are exhausted or if a match is found. This methods
	 * recursively climes ancestor list and interface tree until
	 * 
	 * @param <T>
	 * @param pkgnm
	 * @param superInterface
	 * @param skipAbstract
	 * @return
	 */
	public static <T> Collection<Class<T>> getClasses(String pkgnm,
			String regexp, Class<T> superInterface, boolean skipAbstract) {

		ClassLoader classLoader = Thread.currentThread()
				.getContextClassLoader();
		if (classLoader == null)
			return Collections.emptyList();

		ArrayList<Class<T>> ret = new ArrayList<Class<T>>();

		Collection<String> classNames = getClasses(pkgnm, regexp);
		// System.out.println("Going to check " + classNames.size());
		for (String cn : classNames) {

			if (cn == null)
				continue;
			Class<?> c;
			
//			System.out.println("Checking " + cn);
			
			try {
				c = classLoader.loadClass(cn);
			} catch (Throwable t) {
				continue;
			}
			if (c == null)
				continue;
			// System.out.println("\thave class" + cn);
			// getInterfaces gets only declared interfaces, not all available
			// interfaces, so have to iterate upward.
			LinkedList<Class> ifs = new LinkedList<Class>();
			for (Class<?> t : c.getInterfaces())
				if (t != null)
					ifs.add(t);
			ifs.add(c.getSuperclass());

			while (!ifs.isEmpty()) {
				Class<?> t = ifs.poll();
				if (t != null) {
					// System.out.println("\thave class" + cn
					// + " Checking interface " + t.getName());
					if (t == superInterface
							&& (!skipAbstract || !Modifier.isAbstract(c
									.getModifiers()))) {
						ret.add((Class<T>) c);
						break;
					}
					for (Class<?> t2 : t.getInterfaces())
						ifs.add(t2);
					ifs.add(t.getSuperclass());
				}
			}
		}

		return ret;
	}

	/**
	 * Also copied from some website when searching for how to find classes
	 * inside a package
	 */
	public static Collection<String> getJarClasses(String regexp) {
		return getJarClasses(Pattern.compile(regexp));
	}

	public static Collection<String> getJarClasses(Pattern regexp) {
		ArrayList<String> ret = new ArrayList<String>();

		for (String clss : getJarClasses()) {
			if (regexp.matcher(clss).matches())
				ret.add(clss);
		}

		return ret;
	}

	public static Collection<String> getJarClasses() {

		ArrayList<String> ret = new ArrayList<String>();

		ClassLoader cl = ClassLoader.getSystemClassLoader();
		for (String cp : System.getProperty("java.class.path").split(
				System.getProperty("path.separator"))) {

			File jar = new File(cp);
			if (cp.endsWith(".jar")) {
				// System.out.println("Checking " + cp);
				JarInputStream is;
				try {
					is = new JarInputStream(new FileInputStream(jar));
				} catch (FileNotFoundException e) {
					continue;
				} catch (IOException e) {
					continue;
				}
				// System.out.println("Checking " + cp + " opened fine.");

				// new FileInputStream(jar));
				JarEntry entry;
				do {
					try {
						entry = is.getNextJarEntry();
					} catch (IOException e) {
						break;
					}
					if (entry != null) {
						// System.out.println("Entry is " + entry.getName());
						if (entry.getName().endsWith(".class")
								&& entry.getName().indexOf("$") < 0) {
							// ret.add(Class.forName(entry.getName()));
							String cn = entry.getName();
							cn = cn.substring(0, cn.length() - 6);
							cn = cn.replace('/', '.');
							ret.add(cn);
						}
					}
				} while (entry != null);
			}
		}
		// System.out.println("Returning size " + ret.size());
		return ret;

	}

	/**
	 * Originally copy and pasted from snippets.dzone.com
	 */
	public static Collection<String> getResourseClasses(String packageName) {
		ClassLoader classLoader = Thread.currentThread()
				.getContextClassLoader();
		if (classLoader == null)
			return Collections.emptyList();

		String path = packageName.replace('.', '/');

		Enumeration<URL> resources;
		try {
			resources = classLoader.getResources(path);
		} catch (IOException e) {
			return Collections.emptyList();
		}
		List<File> dirs = new ArrayList<File>();
		while (resources.hasMoreElements()) {
			URL resource = resources.nextElement();
			dirs.add(new File(resource.getFile()));
		}
		ArrayList<String> classes = new ArrayList<String>();
		for (File directory : dirs) {
			classes.addAll(findResourceClasses(directory, packageName, 100));
		}
		return classes;
	}

	private static List<String> findResourceClasses(File directory,
			String packageName, int depthLimit) {
		if (depthLimit <= 0)
			return Collections.emptyList();

		List<String> classes = new ArrayList<String>();
		if (!directory.exists()) {
			return classes;
		}
		File[] files = directory.listFiles();
		for (File file : files) {
			if (file.isDirectory()) {
				assert !file.getName().contains(".");
				classes.addAll(findResourceClasses(file, packageName + "."
						+ file.getName(), depthLimit - 1));
			} else if (file.getName().endsWith(".class")
					&& file.getName().indexOf('$') < 0) {
				classes.add(packageName
						+ '.'
						+ file.getName().substring(0,
								file.getName().length() - 6));
			}
		}
		return classes;
	}

}
